﻿using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System;
using System.Text;

namespace Project362.Users
{
    public class UserHandler
    {

        #region Variables

        /// <summary>
        /// The list of all users for the system.
        /// </summary>
        private Dictionary<string, User> userAccounts;

        /// <summary>
        /// The location of the serialized file containing the users.
        /// </summary>
        private string dictionaryFile;

        #endregion
        #region Constructors

        /// <summary>
        /// Creates a new instance of our user dictionary using the file specified by filename.
        /// </summary>
        /// <param name="filename">The file to load/save the user dictionary to/from.</param>
        public UserHandler(string filename)
        {
            dictionaryFile = filename;
            if (File.Exists(dictionaryFile))
            {
                try
                {
                    Stream stream = File.Open(dictionaryFile, FileMode.Open);
                    BinaryFormatter bf = new BinaryFormatter();
                    userAccounts = (Dictionary<string, User>)bf.Deserialize(stream);
                    stream.Close();
                }
                catch (Exception e)
                {
                    // Exception handle.
                    throw e; // Don't do this. TODO
                }
            }
            else
            {
                userAccounts = new Dictionary<string, User>();
            }
        }

        #endregion
        #region Public Methods

        /// <summary>
        /// Returns the User object with the specified name and password.
        /// </summary>
        /// <param name="userName">The user name.</param>
        /// <param name="password">The user password.</param>
        /// <returns>An instance of User with the given name and password, or null if none exists.</returns>
        public User Login(string userName, string password)
        {
            if (userAccounts.ContainsKey(userName))
            {
                if (VerifyHash(password, userAccounts[userName].Password))
                    return userAccounts[userName];
            }
            return null;
        }

        /// <summary>
        /// Attempts to create a new user with the given name, password, and premission level and add it
        /// to the user dictionary.
        /// </summary>
        /// <param name="userName">The new user name.</param>
        /// <param name="password">The new user password.</param>
        /// <param name="level">The new user permission level.</param>
        /// <returns>True if successfull, false otherwise.</returns>
        public bool AddUser(string userName, string password, PermissionLevel level)
        {
            if (userAccounts.ContainsKey(userName))
                return false;

            string hashValue = Hash(password, null);

            User toAdd = new User(userName, hashValue, level);
            userAccounts.Add(userName, toAdd);
            SaveUserDictionary();
            return true;
        }

        /// <summary>
        /// Attempts to remove the user with the given name from the user dictionary.
        /// </summary>
        /// <param name="userName">The user name (id) to be removed.</param>
        /// <returns>True if successfull, false otherwise.</returns>
        public bool RemoveUser(string userName)
        {
            if (!userAccounts.ContainsKey(userName))
                return false;
            userAccounts.Remove(userName);
            SaveUserDictionary();
            return true;
        }

        /// <summary>
        /// Attempts to change the user permissions of the account with the given name to the given
        /// permission level.
        /// </summary>
        /// <param name="userName">The user name of the account to change.</param>
        /// <param name="level">The new permission level.</param>
        /// <returns>True if successfull, false otherwise.</returns>
        public bool ChangeUserPermissions(string userName, PermissionLevel level)
        {
            if (!userAccounts.ContainsKey(userName))
                return false;
            userAccounts[userName].Permissions = level;
            SaveUserDictionary();
            return true;
        }

        #endregion
        #region Saved Users

        /// <summary>
        /// Serializes the user dictionary.
        /// </summary>
        private void SaveUserDictionary()
        {
            Stream stream = File.Open(dictionaryFile, FileMode.Create);
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(stream, userAccounts);
            stream.Close();
        }

        #endregion
        #region Private Methods

        /// <summary>
        /// Generates a random salt.
        /// </summary>
        /// <param name="min">Minimum salt size.</param>
        /// <param name="max">Maximum salt size.</param>
        /// <returns>The bytes of the salt.</returns>
        private byte[] GenerateSalt(int min, int max)
        {
            Random rand = new Random();
            int saltSize = rand.Next(min, max);
            byte[] bytes = new byte[saltSize];

            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            rng.GetNonZeroBytes(bytes);
            return bytes;
        }

        /// <summary>
        /// Hashes and salts a password.
        /// </summary>
        /// <param name="pw">The password.</param>
        /// <param name="salt">A salt.</param>
        /// <returns>The hashed password with the salt attached.</returns>
        private string Hash(string pw, byte[] salt)
        {

            HashAlgorithm algorithm = new SHA512Managed();
            if (salt == null)
                salt = GenerateSalt(4, 16);
            byte[] plainTextBytes = Encoding.UTF8.GetBytes(pw);
            byte[] plainTextWithSaltBytes = new byte[plainTextBytes.Length + salt.Length];

            for (int i = 0; i < plainTextBytes.Length; i++)
                plainTextWithSaltBytes[i] = plainTextBytes[i];

            for (int i = 0; i < salt.Length; i++)
                plainTextWithSaltBytes[plainTextBytes.Length + i] = salt[i];

            byte[] hashBytes = algorithm.ComputeHash(plainTextWithSaltBytes);
            byte[] hashWithSaltBytes = new byte[hashBytes.Length + salt.Length];

            for (int i = 0; i < hashBytes.Length; i++)
                hashWithSaltBytes[i] = hashBytes[i];

            for (int i = 0; i < salt.Length; i++)
                hashWithSaltBytes[hashBytes.Length + i] = salt[i];

            return Convert.ToBase64String(hashWithSaltBytes);
        }

        /// <summary>
        /// Verifys that a given hashed value is the hashed version of a given
        /// password.
        /// </summary>
        /// <param name="pw">The password.</param>
        /// <param name="hash">The hashed value.</param>
        /// <returns>True if the password is correct, false otherwise.</returns>
        private bool VerifyHash(string pw, string hash)
        {
            byte[] hashWithSaltBytes = Convert.FromBase64String(hash);
            int hashSizeInBytes = 512 / 8;

            if (hashWithSaltBytes.Length < hashSizeInBytes)
                return false;

            byte[] saltBytes = new byte[hashWithSaltBytes.Length - hashSizeInBytes];

            for (int i = 0; i < saltBytes.Length; i++)
                saltBytes[i] = hashWithSaltBytes[hashSizeInBytes + i];

            string expectedHashString = Hash(pw, saltBytes);
            return (hash == expectedHashString);
        }

        #endregion

    }
}
